﻿// <copyright file="IPAddressAuthorisation.cs" company="SharpSTS">
// Copyright (c) 2007, 2008 All Right Reserved, http://sharpsts.com/
//
// This source is subject to the Microsoft Permissive License.
// Please see the License.txt file for more information.
// All other rights reserved.
//
// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY 
// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// </copyright>
// <author>Barry Dorrans</author>
// <email>barryd@idunno.org</email>
// <date>2008-06-13</date>
// <summary>Provides the a simple IP address authorisation facility.</summary>

namespace SharpSTS.CardControl
{
    using System;
    using System.Globalization;
    using System.Net;
    using System.Web;
    using System.Web.Caching;

    using SharpSTS.Configuration;
    using Configuration;

    /// <summary>
    /// Authorises an IP address against the IP subnets listed in the configuration settings.
    /// </summary>
    internal static class IPAddressAuthorisation
    {
        /// <summary>
        /// Provides an object to lock on for syncronisation.
        /// </summary>
        private static readonly object synclock = new object();
        
        /// <summary>
        /// An instance of the current <see cref="HttpRuntime" />
        /// </summary>
        private static HttpRuntime httpRuntime;

        /// <summary>
        /// Gets the asp.net Cache object.
        /// </summary>
        private static Cache Cache
        {
            get
            {
                if (null == httpRuntime)
                {
                    lock (synclock)
                    {
                        if (null == httpRuntime)
                        {
                            httpRuntime = new HttpRuntime();
                        }
                    }
                }

                return HttpRuntime.Cache;
            }
        }

        /// <summary>
        /// Checks if an IP address is authorised against the IP subnets listed in the configuration settings.
        /// </summary>
        /// <param name="ip">The IP address to check</param>
        /// <returns>True if the IP address is auhorised against the IP subnets listed in the configuration settings; otherwise false.</returns>
        /// <exception cref="ArgumentNullException">Thrown if the ip argument is null.</exception>
        public static bool IsAuthorised(IPAddress ip)
        {
            // If the IP address is null then we cannot authorise it.
            if (null == ip)
            {
                throw new ArgumentNullException("ip");
            }

            bool authorised = false;
            string cacheKey = string.Format(CultureInfo.InvariantCulture, "sharpsts_ipCheck_{0}", ip);

            // Check to see if we have a cache result.
            if (null == Cache.Get(cacheKey))
            {
                // If not we need to check the IP.
                foreach (PermittedIPMask mask in STSSettings.Settings.PermittedIPAddressesForCardGeneration)
                {
                    if (!Contains(ip, mask.IPMask))
                    {
                        continue;
                    }

                    authorised = true;
                    break;
                }

                // Add the authorisation result to the cache.
                Cache.Add(cacheKey, authorised, null, Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(15), CacheItemPriority.High, null);
            }

            // Return the result from the cache.
            return ((bool)Cache[cacheKey]);
        }
        
        /// <summary>
        /// Checks if the specified IP address is within the specified subnet.
        /// </summary>
        /// <param name="ip">The <see cref="IPAddress"/> to check.</param>
        /// <param name="subnet">The subnet to check</param>
        /// <returns>True if the subnet contains the specified IP address, otherwise false.</returns>
        /// <exception cref="ArgumentNullException">Thrown if the ip or subnetmask are null.</exception>
        [System.Diagnostics.CodeAnalysis.SuppressMessage(
            "Microsoft.Design", 
            "CA1031:DoNotCatchGeneralExceptionTypes",
            Justification = "As this is a security method we catch all exceptions and default to false on purpose.")]
        private static bool Contains(IPAddress ip, string subnet)
        {
            if (null == ip)
            {
                throw new ArgumentNullException("ip");
            }

            if (string.IsNullOrEmpty(subnet))
            {
                throw new ArgumentNullException("subnet");
            }

            try
            {
                byte[] subnetBytes;
                byte[] netmaskBytes;
                Parse(subnet, out subnetBytes, out netmaskBytes);

                byte[] address = ip.GetAddressBytes();

                // Check each byte if the IP address matches the subnet
                for (int i = 0; i < address.Length; i++)
                {
                    if ((address[i] & netmaskBytes[i]) != subnetBytes[i])
                    {
                        return false;
                    }
                }

                return true;
            }
            catch
            {
                return false;
            }
        }

        /// <summary>
        /// Parses the given subnet string into a subnet and netmask pair.
        /// </summary>
        /// <param name="subnetString">The string to parse.</param>
        /// <param name="subnet">The returned subnet.</param>
        /// <param name="netmask">The returned netmask.</param>
        /// <exception cref="ArgumentNullException">Thrown if the subnetString is null or empty.</exception>
        private static void Parse(string subnetString, out byte[] subnet, out byte[] netmask)
        {
            if (string.IsNullOrEmpty(subnetString))
            {
                throw new ArgumentNullException("subnetString");
            }

            string[] subnetParts = subnetString.Split('/');

            if (subnetParts.GetUpperBound(0) != 1 ||
                string.IsNullOrEmpty(subnetParts[0]) ||
                string.IsNullOrEmpty(subnetParts[1]))
            {
                throw new ArgumentException("Not a valid subnet string", "subnetString");
            }

            IPAddress subnetIP = IPAddress.Parse(subnetParts[0]);
            if (null == subnetIP)
            {
                throw new ArgumentException("Not a valid subnet string", "subnetString");
            }

            subnet = subnetIP.GetAddressBytes();
            netmask = subnetParts.Length > 1 ? ParseNetmask(subnetParts[1]) : ParseNetmask("255.255.255.255");
        }

        /// <summary>
        /// Parses a netmask into a byte array.
        /// </summary>
        /// <param name="netmask">The netmask to parse.</param>
        /// <returns>A byte array representing the netmask.</returns>
        /// <exception cref="ArgumentNullException">Thrown if the netmask is null or empty.</exception>
        private static byte[] ParseNetmask(string netmask)
        {
            if (string.IsNullOrEmpty(netmask))
            {
                throw new ArgumentNullException("netmask");
            }

            if (netmask.IndexOf(".", StringComparison.Ordinal) > 0)
            {
                IPAddress ipAddress = IPAddress.Parse(netmask);
                return ipAddress != null ? ipAddress.GetAddressBytes() : new byte[] { 255, 255, 255, 255 };
            }

            throw new ArgumentException("invalid netmask format", "netmask");
        }
    }
}
